﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

namespace Yappi
{
    /// <summary>
    /// Provides fast access to TType's properties by its name.
    /// All accesses done as is e.g. callvirt to virtual properties and call to others.
    /// All operations in this class is O(1) except first access wich takes O(N) where N is number of properties in TType.
    /// All methods a thread safe.
    /// All accesses (except first one) are lock-free.
    /// </summary>
    /// <typeparam name="TType">Type, to construct property accessors.</typeparam>
    public static class Property<TType>
    {
// ReSharper disable StaticFieldInGenericType
        private static volatile Dictionary<String, Delegate> _getters;
        private static volatile Dictionary<String, Delegate> _setters;
        private static readonly Object SyncRoot = new object();
// ReSharper restore StaticFieldInGenericType

        private static Dictionary<String,Delegate> Getters
        {
            get
            {
                if(_getters == null)
                {
                    lock (SyncRoot)
                    {
                        if(_getters == null)
                        {
                            _getters = CompileGetters();
                        }
                    }
                }
                return _getters;
            }
        }

        private static Dictionary<String, Delegate> Setters
        {
            get
            {
                if (_setters == null)
                {
                    lock (SyncRoot)
                    {
                        if (_setters == null)
                        {
                            _setters = CompileSetters();
                        }
                    }
                }
                return _setters;
            }
        }

        public static Func<TType,TValue> GetGetter<TValue>(string propertyName)
        {
            return (Func<TType,TValue>)Getters[propertyName];
        }
        public static Action<TType,TValue> GetSetter<TValue>(string propertyName)
        {
            return (Action<TType, TValue>)Setters[propertyName];
        }
        public static TValue Get<TValue>(TType instance,string propertyName)
        {
            return GetGetter<TValue>(propertyName)(instance);
        }

        public static void Set<TValue>(TType instance, string propertyName, TValue value)
        {
            GetSetter<TValue>(propertyName)(instance, value);
        }

        private static Dictionary<String,Delegate> CompileGetters()
        {
            var type = typeof (TType);
            var propertys = type.GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance);
            var getters = new Dictionary<String, Delegate>(propertys.Length);

            foreach (var property in propertys)
            {
                if(property.CanRead)
                {
                    var parameter = Expression.Parameter(type, "instance");
                    var lambda = Expression.Lambda(Expression.Call(parameter,property.GetGetMethod()), parameter);
                    getters.Add(property.Name,lambda.Compile());
                }
            }
            return getters;
        }

        private static Dictionary<String, Delegate> CompileSetters()
        {
            var type = typeof(TType);
            var propertys = type.GetProperties(BindingFlags.SetProperty|BindingFlags.Public|BindingFlags.Instance);
            var setters = new Dictionary<String, Delegate>(propertys.Length);

            foreach (var property in propertys)
            {
                if (property.CanWrite)
                {
                    var instance = Expression.Parameter(type, "instance");
                    var value = Expression.Parameter(property.PropertyType, "value");
                    var lambda = Expression.Lambda(Expression.Call(instance,property.GetSetMethod(),value),instance,value);
                    setters.Add(property.Name, lambda.Compile());
                }
            }
            return setters;
        }
    }
}
